Schema definition topic
For package:typed_sql
to offer a convenient type-safe API for writing SQL
queries we need to define the database schema and generate code from it.
Database Schema
The schema must be defined in a single Dart library, this is typically called
model.dart
, the generated part will be written to model.g.dart
using
build_runner. Thus, we must always include this part-file as follows:
import 'package:typed_sql/typed_sql.dart';
part 'model.g.dart';
We must then define a schema class. This is an abstract final class
extending Schema, specifying what tables the database has.
If we were working on a bookstore we might define a schema as follows:
abstract final class Bookstore extends Schema {
Table<Author> get authors;
Table<Book> get books;
}
The schema class is only allowed to have abstract getters that returns
Table objects. Each such getter defines a table in the database.
The T
in Table<T>
must be a row class specifying the table layout.
Note
Each table must have its own row class. It's not possible to reuse the same row class for multiple tables.
Row class for Author
For each table in our database we must define a row class. This is an
abstract final class
extending the Row
class, specifying what fields
the database table has. Continuing with the bookstore example we can define a
row class for the authors
table as follows:
@PrimaryKey(['authorId'])
abstract final class Author extends Row {
@AutoIncrement()
int get authorId;
@Unique()
String get name;
}
The Author
row class specifies a table with two fields:
authorId
, and,name
.
The authorId
will be a 64 bit integer, auto-incremented by default and used
as primary key. The name
field will be TEXT
and have two constraints
NOT NULL
(because the Dart getter isn't nullable) and UNIQUE
because of
the @Unique()
annotation.
The equivalent SQL depends on the database, but it looks something like:
CREATE TABLE authors (
authorId INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
name TEXT NOT NULL,
UNIQUE (name)
);
The following data types are allowed for fields:
bool
,int
,double
,String
,DateTime
,Uint8List
, and,- Custom subclasses of
CustomDataType<T>
.
These types are allowed to be nullable and non-nullable. When fields are
non-nullable in the Dart row class the SQL table will have a NOT NULL
constraint. For details on CustomDataType see
Custom data types documentation.
Note
These row classes are intended to be data classes, they may not have constructors or members other than abstract public getters. Nor can they subclass or implement other classes.
If you want to add custom helper methods, you may write extension methods for your row classes.
Book
row class with foreign key
Returning to our bookstore example, we still need to define a Book
row class for the books
table in the Bookstore
schema. If we want the
books
table to have a foreign key referencing the authors
table we can
define the Book
row class as follows:
@PrimaryKey(['bookId'])
abstract final class Book extends Row {
@AutoIncrement()
int get bookId;
String? get title;
@References(
// This fields references "authorId" from "authors" table
table: 'authors',
field: 'authorId',
// The reference is _named_ "author", this gives rise to a
// Expr<Book>.author property when building queries.
name: 'author', // optional
// This is referenced _as_ "books", this gives rise to a
// Expr<Author>.books property when building queries.
as: 'books', // optional
)
int get authorId;
@DefaultValue(0)
int get stock;
}
The @DefautValue(0)
annotation gives the stock
field a default value of 0
.
This also makes the stock
field optional when inserting rows.
Note
The name
and as
properties in the @References
annotation are optional.
These gives rise to convinient subquery properties we can use when writing
queries later.
The equivalent SQL depends on the database, but it looks something like:
CREATE TABLE books (
bookId BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
title TEXT,
authorId BIGINT NOT NULL,
stock BIGINT NOT NULL DEFAULT 0,
FOREIGN KEY (authorId) REFERENCES authors (authorId)
);
Notice that because the title
field is nullable, it does not have a NOT NULL
constraint in the database.
Generating code
Whenever the definition of the database schema is changed, it's important to
run code generation. This is done using build_runner. If you don't have a
dev-dependency on build_runner
you may add this as follows:
dart pub add dev:build_runnner
You can run code-generation by invoking build_runner as follows:
dart run build_runner build
This should create or update the model.g.dart
part-file for your model.dart
library.
Using the database
When we've defined a schema and generated code we can start using the database. We must first create a Database instance using a DatabaseAdapter and SqlDialect as follows:
final db = Database<Bookstore>(adapter, dialect);
How you obtain a DatabaseAdapter and which SqlDialect to use depends on your database, as well as how you are connecting.
Once you have Database<Bookstore>
instance you can create empty tables for
your schema as follows:
// Create tables
await db.createTables();
Creating empty tables from scratch is mostly useful for testing, it's rarely
needed in production. Instead you can use the generated create<Schema>Tables
,
which outputs the DDL for creating the tables.
// Get the database schema
final ddl = createBookstoreTables(SqlDialect.postgres());
This can then be used with external migration management tools to managing database migrations. See Migrations documentation.
Note
The astute reader might notice that we never actually create an instance
of Bookstore
schema class. Instead the class is used to define a type we
can parameterize the Database instance with.
This is intentional to avoid juggling constructors, but it might be revised
in the future.
With a Database<Bookstore>
and tables created through migrations or
db.createTables()
you can insert data into the database as follows:
// Insert a row into the "authors" table
final author = await db.authors
.insert(
name: toExpr('Easter Bunny'),
)
.returnInserted()
.executeAndFetch(); // returns Future<Author?>
// Insert a row into the "books" table
await db.books
.insert(
title: toExpr('How to hide eggs'),
authorId: toExpr(author.authorId),
)
.execute();
Now we can also write queries against the database. The following demonstrates
how to write a query that filters on the book title and only returns title
and author.name
.
// Query for books where the title contains 'eggs'
// select the title and author name
final titleAndAuthor = await db.books
.where(
(book) => book.title
.orElseValue('') // because title can be null
.toLowerCase()
.containsValue('eggs'),
)
.select(
(book) => (
book.title,
book.author.name, // use the 'author' subquery property
),
)
.fetch();
// Compare the results
check(titleAndAuthor).unorderedEquals([
('How to hide eggs', 'Easter Bunny'),
]);
The astute reader might notice that author.name
is not actually a property on
the books
table. However, because we defined a foreign key on Book
row class with the @References
annotation, and gave it the name author
,
the Expr<Book>
expression gets a subquery property book.author
that allows
us to access properties on the referenced authors
row.
See References documentation for details.
Classes
- AutoIncrement Schema definition
- Annotation for a field that should be auto-incremented (by default).
-
CustomDataType<
T extends Object?> Schema definition Custom data types - Interface to be implemented by custom types that can be stored in a Row for automatic (de)-serialization.
-
DefaultValue<
T> Schema definition - Annotation for a field that has a default value, specified by value.
- ForeignKey Schema definition Foreign keys
- Annotation for declaring a composite foreign key.
- PrimaryKey Schema definition
- Annotation for a table specifying its primary key.
- References Schema definition Foreign keys
- Annotation for fields that references fields from another table.
- Row Schema definition
- Marker class which all row classes must extend.
- Schema Schema definition Migrations
- Marker class which all schema definitions must extend.
- Unique Schema definition
- Annotation for a unique field.